package org.koin.core.registry;

import org.koin.core.Koin;
import org.koin.core.annotation.KoinInternalApi;
import org.koin.core.definition.BeanDefinition;
import org.koin.core.instance.FactoryInstanceFactory;
import org.koin.core.instance.InstanceContext;
import org.koin.core.instance.InstanceFactory;
import org.koin.core.instance.SingleInstanceFactory;
import org.koin.core.logger.Level;
import org.koin.core.parameter.ParametersDefinition;
import org.koin.core.scope.Scope;
import java.util.*;
import java.util.function.Consumer;

@KoinInternalApi
public class InstanceRegistry {

    private Koin _koin;
    private Scope _scope;
    private HashMap<String, InstanceFactory> _instances = new HashMap<String, InstanceFactory>();

    public InstanceRegistry(Koin _koin, Scope _scope) {
        this._koin = _koin;
        this._scope = _scope;
    }

    public void create(Set<BeanDefinition> definitions) {
        for (BeanDefinition definition : definitions) {
            if (_koin.getLogger().isAt(Level.DEBUG)) {
                if (_scope.getScopeDefinition().isRoot()) {
                    _koin.getLogger().debug("- " + definition);
                } else {
                    _koin.getLogger().debug(_scope + " -> " + definition);
                }
            }
            saveDefinition(definition, false);
        }
    }

    public void saveDefinition(BeanDefinition definition, boolean override) {
        boolean defOverride = definition.getOptions().isOverride() || override;
        InstanceFactory instanceFactory = createInstanceFactory(_koin, definition);
        saveInstance(
                BeanDefinition.indexKey(definition.getPrimaryType(), definition.getQualifier()),
                instanceFactory,
                defOverride
        );

        int typesCount = definition.getSecondaryTypes().size();
        for (int i = 0; i < typesCount; i++) {
            if (defOverride) {
                saveInstance(
                        BeanDefinition.indexKey((Class) definition
                                .getSecondaryTypes()
                                .get(i),
                                definition.getQualifier()),
                        instanceFactory,
                        defOverride
                );
            } else {
                saveInstanceIfPossible(
                        BeanDefinition.indexKey((Class) definition
                                .getSecondaryTypes()
                                .get(i),
                                definition.getQualifier()),
                        instanceFactory
                );
            }
        }
    }

    private InstanceFactory createInstanceFactory(Koin koin, BeanDefinition definition) {
        switch (definition.getKind()) {
            case Single:
                return new SingleInstanceFactory(_koin, definition);
            case Factory:
                return new FactoryInstanceFactory(_koin, definition);
        }
        return null;
    }

    private void saveInstance(String key, InstanceFactory factory, boolean override) {
        if (_instances.containsKey(key) && !override) {
            throw new IllegalStateException("InstanceRegistry already contains index '" + key + "'");
        } else {
            _instances.put(key, factory);
        }
    }

    private void saveInstanceIfPossible(String key, InstanceFactory factory) {
        if (!_instances.containsKey(key)) {
            _instances.put(key, factory);
        }
    }

    public <T> T resolveInstance(String indexKey, ParametersDefinition parameters) {
        InstanceFactory instanceFactory = _instances.get(indexKey);
        if (instanceFactory != null) {
            return (T) instanceFactory.get(defaultInstanceContext(parameters));
        }
        return null;
    }

    private InstanceContext defaultInstanceContext(ParametersDefinition parameters) {
        return new InstanceContext(_koin, _scope, parameters);
    }

    public void close() {
        _instances.values().forEach(new Consumer<InstanceFactory>() {
            @Override
            public void accept(InstanceFactory factory) {
                factory.drop();
            }
        });
        _instances.clear();
    }

    public void createEagerInstances() {
        Collection<InstanceFactory> values = _instances.values();
        for (InstanceFactory item : values) {
            if (item instanceof SingleInstanceFactory) {
                if (item.getBeanDefinition().getOptions().isCreatedAtStart()) {
                    item.get(new InstanceContext(_koin, _scope));
                }
            }
        }
    }

    public <T> List<T> getAll(Class clazz) {
        Collection<InstanceFactory> values = _instances.values();
        Set<InstanceFactory> instances = new HashSet<>();
        instances.addAll(values);
        List<InstanceFactory> potentialKeys = new ArrayList<>();
        for (InstanceFactory instance : instances) {
            if (instance.getBeanDefinition().hasType(clazz)) {
                potentialKeys.add(instance);
            }
        }
        List<T> result = new ArrayList<>();
        potentialKeys.forEach(new Consumer<InstanceFactory>() {
            @Override
            public void accept(InstanceFactory factory) {
                T item = (T) factory.get(defaultInstanceContext(null));
                if (item != null) {
                    result.add(item);
                }
            }
        });
        return result;
    }

    public <S> S bind(
            Class primaryType,
            Class secondaryType,
            ParametersDefinition parameters
    ) {

        for (InstanceFactory instance : _instances.values()) {
            boolean canBind = instance.getBeanDefinition().canBind(
                    primaryType,
                    secondaryType
            );
            if (canBind) {
                return (S) instance.get(defaultInstanceContext(parameters));
            }
        }
        return null;
    }

    public void dropDefinition(BeanDefinition definition) {
        Set<String> keys = _instances.keySet();
        List<String> ids = new ArrayList<>();
        for (String key : keys) {
            boolean equals = _instances.get(key).getBeanDefinition().equals(definition);
            if (equals) {
                ids.add(key);
            }
        }
        ids.forEach(new Consumer<String>() {
            @Override
            public void accept(String s) {
                _instances.remove(s);
            }
        });
    }

    public void createDefinition(BeanDefinition definition) {
        saveDefinition(definition, definition.getOptions().isOverride());
    }

    public HashMap<String, InstanceFactory> getInstances() {
        return _instances;
    }
}
